Object oriented languages, such as C++ and Java, have emerged as the preferred tools for programmers. These programming languages allow data structures and algorithms to be expressed in a way that is easy to understand and they are able to execute in a predictable way on various computing platforms. The languages have also been widely adopted because of their so-called modularity. Indeed, a major premise for developing the Java language has been to put programmer productivity before any other concern, as eloquently captured in the now famous Java slogan, “Write Once, Run Anywhere”.
Java is a widely used programming language due to the machine independent nature of bytecodes. In addition to portability, both security and ease of development of applications have made Java very popular with the software community. Since the specifications offer substantial flexibility in the implementation of the Java virtual machine, a number of techniques have been used to execute bytecodes. The most commonly used modes of execution are interpretation, a mode that interprets the bytecodes, and just-in-time compilation, a mode that dynamically translates bytecodes to native code at runtime. A recent development has been the hybrid execution engine, which uses interpreters, adaptive just-in-time compilers, ahead-of-time compilers and profile-based feedback to interpret/compile bytecodes. Other possible modes include hardware execution of bytecodes.
In the Java execution model, each source code class is compiled to the Java virtual machine class file format and methods are compiled into a fully portable standard bytecode representation. To execute a program operation, such as a method or a function, the virtual machine translates its bytecode instructions into an execution sequence of (hardware) machine code. The translation of a bytecode instruction typically involves accessing the arguments of the instruction, implementing the function of the instruction, and dispatching (fetching, decoding and starting) the next instruction.
Java interpreters are generally highly portable and rapid to implement. They are considerably smaller and simpler than just-in-time compilers, and this makes them quicker to develop, less expensive to maintain and potentially more reliable. The problem with existing interpreters, however, is that they run most code slower than compilers.
Just-in-time compilers interact with the virtual machine at run time and compile appropriate bytecode sequences into native machine code. Through just-in-time compilation, a bytecode method is translated into a native method on the fly, so as to replace (probably repeated) interpretation overhead with (probably one-time) compilation overhead. Unlike a traditional compiler, in which compile time is often ignored, a just-in-time compiler needs to produce code rather quickly. This typically requires optimization algorithms to be small and efficient.
An adaptive just-in-time compiler monitors execution and modifies the code to keep it correct and to optimize it. In particular, adaptive compilation allows inlining to be used extensively where the targets of most method calls cannot be determined at compile time, even when compilation is just-in-time. Under an adaptive just-in-time compiler, code may be recompiled many times as the compiler optimizes the code or adapts it to changing conditions.
Combined with execution profiling, a just-in-time compiler can dramatically increase the execution speed of Java programs by adapting the code to changing conditions. Execution profiling may occur during the execution of the bytecode. The runtime system or virtual machine connects a profiler to a process during process initialization. The profiler receives notifications each time that a function is entered or exited, as well as when other events occur that are of interest. By gathering statistics on these events, a profiler can build a comprehensive picture of which routines used the most CPU time, when garbage collections occurred, if exceptions were thrown, and the like. Profiling the program's behavior during execution can assist the virtual machine in identifying paths for optimization, such as which code is likely to be subject to heavy use. This code can be pre-compiled and cached by an ahead-of-time compiler to reduce runtime overhead.
Although just-in-time compilers eliminate interpretation overhead, they are typically considered inappropriate in the context of real-time systems because the overall execution time of the code increases. Real-time applications are those for which timeliness, that is, the ability to guarantee that an event will occur at a suitable time, needs to be guaranteed. The real-time Java platform dictates that the worst-case performance of a method must meet specified timing constraints. This means that a just-in-time or adaptive just-in-time compiler only benefits real-time applications if its impact can be isolated to places where real-time constraints are relaxed. If compilation takes place during a time-critical interval, it will almost certainly increase the execution time for that code compared to a simple interpreter. Consequently, the benefits of just-in-time compilation in real-time systems do not outweigh the increase in execution overhead.
Certain emerging real-time standards, such as the Real-time Specification for Java (RTSJ), have been designed to provide real-time programming features for the Java environment. The RTSJ, for instance, extends the Java memory model by providing memory areas other than the heap. These memory areas are often characterized by the lifetime of the objects created in them, as well as the time taken for their allocation.
The RTSJ provides different classes of memory, including immortal memory, scoped memory and heap memory. Immortal memory is a memory resource that is shared among schedulable objects and is not normally subject to garbage collection. Objects allocated in immortal memory exist until the application terminates. Unlike the standard Java heap, immortal objects continue to exist even after there are no other references to them. Scoped memory is a class of memory dealing with representations of memory spaces with a limited lifetime. The scoped memory area is valid as long as there is a schedulable object with access to it. Scoped memory is also free from garbage collection. The RTSJ supports the normal types of memory, including heap memory and local variables.
The RTSJ uses assignment and reference checks that are applied to scoped and immortal memory regions in order to maintain referential integrity in the Java environment. These assignment and reference checks ensure memory safety by, among other things, preventing schedulable objects from interfering with the garbage collector and avoiding dangling references. Some memory reference checking can be performed at compile time by pointer escape analysis. Due to the unpredictable nature of schedulable objects, however, some checks need to be performed at runtime.
Assignment checking prevent the creation of dangling references by forbidding assignments that could cause a field of an object to point to an object with a shorter lifetime. The RTSJ permits references to objects in heap or immortal memory to be stored in any class of memory. A scope memory object, therefore, can reference objects in the heap. The garbage collector typically ensures that objects in the heap survive at least as long as references to those objects. Objects in a scope are freed when the scope is no longer active. If an object in heap or immortal memory were permitted to hold a reference to an object in a scoped memory location, that reference would become invalid once the scope is exited. Consequently, objects in heap or immortal memory cannot contain references to scope memory areas. Runtime checks of scoped memory references can be performed to enforce the rules.
The following is a set of assignment checking rules that apply to scoped memory area objects:                1. A reference to an object in scoped memory cannot be stored in an object that is allocated in the heap.        2. A reference to an object in scoped memory cannot be stored in an object that is allocated in immortal memory.        3. A reference to an object in scoped memory can only be stored in objects that are allocated in the same scoped memory area or to a more “inner” scope memory area.        4. References to immortal or heap objects may be stored in an object that is allocated in a scoped memory area.        
These rules can be enforced if the Java virtual machine implements several logical rules when storing object references, as explained in Table 1.
TABLE 1ReferenceReferenceto Heapto ImmortalReference to ScopeHeapYesYesNoImmortalYesYesNoScopeYesYesYes, if same or outer scopeLocal VariableYesYesYes, if same or outer scope
The RTSJ also extends the Java thread model to include additional schedulable classes: real-time threads, no-heap real-time threads, and async event handlers (which may choose to operate without access to the heap). These classes are collectively called schedulable objects. No-heap real-time threads and no-heap async event handlers can preempt the garbage collector. The RTSJ does not allow a no-heap domain to load or store a reference to an object in the heap. It can, however, replace references to the heap with references to no-heap objects or null. If the reference to the heap is not handled appropriately, garbage collection on the heap object may be delayed.
There are various approaches used to implement assignment and reference checks. A just-in-time compiler, for example, may be used to generate code that determines the state of a schedulable object before each schedulable object-state-dependent operation. As discussed above, however, just-in-time compilers may be impractical in resource-constrained environments. Another approach involves requiring all schedulable objects to generate their own code for reference checking. No-heap real-time threads, for instance, can be designed to generate code for no-heap reference checking. This code generation, however, can result in substantial processing overhead.
Another technique uses the Java virtual machine to verify compliance with the rules at each bytecode instruction that stores an object reference. This runtime checking usually involves first determining which tests are applicable to the state of the schedulable object executing the byte code, then inspecting the arguments as appropriate for the domain, and if the operation violates a rule, throwing an illegal assignment error exception, or a reference error. Unfortunately, as with the other techniques, this approach causes excessive processing overhead.
Yet another approach uses a trusted compiler to verify the correctness of assignment operations. In a dynamically bound, multi-threaded environment, however, only a run-time test can catch all possible violations of memory assignments, since any number of schedulable objects may be active in a section of code at a given time. Thus, most rule checking needs to occur at runtime, which causes degraded performance.
Efficient translation/execution in a resource-constrained environment remains a fundamental problem in the field. High levels of runtime checking and exception processing overhead can significantly degrade application responsiveness and determinism. Minimizing this overhead is therefore an important goal of a real-time system.